﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Wc.PubSub
{
	/// <summary>
	/// A synchronized keyed collection that allows subscribers.
	/// See remarks for usage example.
	/// </summary>
	/// <typeparam name="TKey"></typeparam>
	/// <typeparam name="TItem"></typeparam>
	/// <remarks>
	/// <example>
	/// Example usage:
	/// <code><![CDATA[
	/// 
	/// class FooSubscriber : IFeedSubscriber<int, Foo>
	/// {
	///		IDisposable _subscription;
	///		
	///		public void Subscribe()
	///		{
	///			// Assuming Server.FooFeed is an instance of KeyedCollectionFeed<TKey, TItem>.
	///			_subscription = Server.FooFeed.Subscribe(this);
	///		}
	///		
	///		public void Unsubscribe()
	///		{
	///			_subscription.Dispose();
	///		}
	///		
	///		void FeedUpdate(IEnumerable<TItem> changedItems, IEnumerable<TKey> removedKeys)
	///		{
	///			// Handle added/updated and removed items...
	///		}
	/// }
	/// 
	/// ]]></code>
	/// </example>
	/// </remarks>
	public class KeyedCollectionFeed<TKey, TItem> : SynchronizedKeyedCollection<TKey, TItem>, IDisposable
	{
		#region Constructor

		/// <summary>
		/// Value that is required to be passed to the base constructor to define the amount of items
		/// that can be added before an internal dictionary is created to hold keys.
		/// This class requires that the value be zero so that the dictionary is created immediately
		/// upon the first item being added.
		/// </summary>
		const int RequiredDictionaryCreationThreshold = 0;

		protected KeyedCollectionFeed()
			: base(new object(), EqualityComparer<TKey>.Default, RequiredDictionaryCreationThreshold) { }

		protected KeyedCollectionFeed(IEqualityComparer<TKey> comparer)
			: base(new object(), comparer, RequiredDictionaryCreationThreshold) { }
		/// <summary>
		/// Creates a <see cref="KeyedCollectionFeed{TKey, TItem}"/> that uses the given delegate to get a key for a given item.
		/// </summary>
		/// <param name="fnGetKeyForItem"></param>
		public KeyedCollectionFeed(Func<TItem, TKey> fnGetKeyForItem)
			: base(new object(), EqualityComparer<TKey>.Default, RequiredDictionaryCreationThreshold)
		{
			_fnGetKeyForItem = fnGetKeyForItem;
		}
		/// <summary>
		/// Creates a <see cref="KeyedCollectionFeed{TKey, TItem}"/> that uses the given delegate to get a key for a given item
		/// and uses the given <see cref="IEqualityComparer{TKey}"/> to compare keys.
		/// </summary>
		/// <param name="fnGetKeyForItem"></param>
		/// <param name="comparer"></param>
		public KeyedCollectionFeed(Func<TItem, TKey> fnGetKeyForItem, IEqualityComparer<TKey> comparer)
			: base(new object(), comparer, RequiredDictionaryCreationThreshold)
		{
			_fnGetKeyForItem = fnGetKeyForItem;
		}

		#endregion

		#region Keys

		/// <summary>
		/// A delegate to return the key for a given item.
		/// </summary>
		Func<TItem, TKey> _fnGetKeyForItem;
		/// <summary>
		/// Returns the key for the given item.
		/// </summary>
		/// <param name="item"></param>
		/// <returns></returns>
		protected override TKey GetKeyForItem(TItem item)
		{
			return _fnGetKeyForItem(item);
		}

		#endregion

		#region Item Management

		/// <summary>
		/// Adds or updates an item. Returns true if the item was added.
		/// </summary>
		/// <param name="item"></param>
		/// <returns>True, if the item was added. Otherwise, false.</returns>
		public bool AddOrUpdate(TItem item)
		{
			lock (base.SyncRoot)
			{
				/// 
				/// NOTE: If there are any items in the collection,
				/// then we can rest assured that base.Dictionary will exist.
				/// See summary of RequiredDictionaryCreationThreshold.
				/// 
				var dic = base.Dictionary;

				if (dic != null)
				{
					var key = GetKeyForItem(item);
					if (dic.ContainsKey(key))
					{
						int index = base.IndexOf(item);
						SetItem(index, item);
						return false;
					}
					else
					{
						Add(item);
						return true;
					}
				}
				else
				{
					Add(item);
					return true;
				}
			}
		}
		/// <summary>
		/// Adds or updates an item. Returns true if the item was added.
		/// </summary>
		/// <param name="item"></param>
		/// <param name="replacedItem"></param>
		/// <returns>True, if the item was added. Otherwise, false.</returns>
		public bool AddOrUpdate(TItem item, out TItem replacedItem)
		{
			lock (base.SyncRoot)
			{
				/// 
				/// NOTE: If there are any items in the collection,
				/// then we can rest assured that base.Dictionary will exist.
				/// See summary of RequiredDictionaryCreationThreshold.
				/// 
				var dic = base.Dictionary;

				if (dic != null)
				{
					var key = GetKeyForItem(item);
					if (dic.TryGetValue(key, out replacedItem))
					{
						int index = base.IndexOf(item);
						SetItem(index, item);
						return false;
					}
					else
					{
						Add(item);
						return true;
					}
				}
				else
				{
					replacedItem = default(TItem);
					Add(item);
					return true;
				}
			}
		}
		/// <summary>
		/// Override of <see cref="SynchronizedCollection{TItem}.ClearItems"/> to notify subscribers of removals.
		/// </summary>
		protected override void ClearItems()
		{
			var dic = base.Dictionary;
			if (dic != null)
			{
				var keys = dic.Keys.ToArray();
				foreach (var sub in _subs.Values) sub.ItemsRemoved(keys);
			}
			base.ClearItems();
		}
		/// <summary>
		/// Override of <see cref="SynchronizedCollection{TItem}.InsertItem"/> to insert the item and notify subscribers.
		/// </summary>
		/// <param name="index"></param>
		/// <param name="item"></param>
		protected override void InsertItem(int index, TItem item)
		{
			/// 
			/// NOTE: A lock on base.SyncRoot is already held by the calling code in the base class.
			/// 
			base.InsertItem(index, item);

			foreach (var sub in _subs.Values) sub.ItemChanged(item);
		}
		/// <summary>
		/// Override of <see cref="SynchronizedCollection{TItem}.RemoveItem"/> to remote the item and notify subscribers.
		/// </summary>
		/// <param name="index"></param>
		protected override void RemoveItem(int index)
		{
			/// 
			/// NOTE: A lock on base.SyncRoot is already held by the calling code in the base class.
			/// 
			TKey key = GetKeyForItem(Items[index]);

			base.RemoveItem(index);

			foreach (var sub in _subs.Values) sub.ItemRemoved(key);
		}
		/// <summary>
		/// Override of <see cref="SynchronizedCollection{TItem}.SetItem"/> to insert the item and notify subscribers.
		/// </summary>
		/// <param name="index"></param>
		/// <param name="item"></param>
		protected override void SetItem(int index, TItem item)
		{
			base.SetItem(index, item);

			foreach (var sub in _subs.Values) sub.ItemChanged(item);
		}
		/// <summary>
		/// Tries to extract an item from the collection.
		/// </summary>
		/// <param name="key"></param>
		/// <param name="value"></param>
		/// <returns>True, if an item by the given key was found and removed.</returns>
		public bool TryExtract(TKey key, out TItem value)
		{
			lock (base.SyncRoot)
			{
				if (!TryGetValue(key, out value)) return false;
				Remove(key);
				return true;
			}
			
		}

		#endregion

		#region Item Access

		/// <summary>
		/// Gets the value associated with the given key.
		/// </summary>
		/// <param name="key"></param>
		/// <param name="value"></param>
		/// <returns></returns>
		public bool TryGetValue(TKey key, out TItem value)
		{
			lock(base.SyncRoot)
			{
				/// 
				/// NOTE: If there are any items in the collection,
				/// then we can rest assured that base.Dictionary will exist.
				/// See summary of RequiredDictionaryCreationThreshold.
				/// 
				var dic = base.Dictionary;

				if (dic != null)
					return base.Dictionary.TryGetValue(key, out value);
				else
				{
					value = default(TItem);
					return false;
				}
			}
		}

		#endregion

		#region Subscription

		/// <summary>
		/// Represents a subscribers subscription.
		/// The subscription is ended by calling <see cref="Dispose"/>.
		/// </summary>
		class Subscription : IDisposable
		{
			readonly KeyedCollectionFeed<TKey, TItem> _feed;

			public readonly int Id;

			readonly IFeedSubscriber<TKey, TItem> _subscriber;

			public Subscription(KeyedCollectionFeed<TKey, TItem> feed, int id, IFeedSubscriber<TKey, TItem> subscriber)
			{
				_feed = feed;
				Id = id;
				_subscriber = subscriber;
			}

			public void ItemChanged(TItem item)
			{
				_subscriber.FeedUpdate(new TItem[] { item }, null);
			}

			public void ItemsChanged(IEnumerable<TItem> items)
			{
				_subscriber.FeedUpdate(items, null);
			}

			public void ItemRemoved(TKey key)
			{
				_subscriber.FeedUpdate(null, new TKey[] { key });
			}

			public void ItemsRemoved(IEnumerable<TKey> keys)
			{
				_subscriber.FeedUpdate(null, keys);
			}

			#region IDisposable

			bool _disposed;

			public void Dispose()
			{
				if (_disposed) return;
				_disposed = true;

				_feed.Unsubscribe(Id);
			}

			#endregion
		}

		#endregion

		#region Subscription Management

		/// <summary>
		/// Integer that is used to generate subscription identifiers.
		/// </summary>
		int _subIdSequence;
		/// <summary>
		/// The set of current subscribers, keyed by <see cref="Subscription.Id"/>.
		/// </summary>
		readonly Dictionary<int, Subscription> _subs = new Dictionary<int,Subscription>(4);
		/// <summary>
		/// Adds the given subscriber to the list of subscribers and returns a
		/// disposable subscription object that may be disposed when the subscriber
		/// wishes to cancel the subscription.
		/// </summary>
		/// <param name="subscriber"></param>
		/// <returns></returns>
		public IDisposable Subscribe(IFeedSubscriber<TKey, TItem> subscriber)
		{
			Subscription sub = null;
			lock (base.SyncRoot)
			{
				sub = new Subscription(this, _subIdSequence++, subscriber);
				_subs.Add(sub.Id, sub);
				subscriber.FeedUpdate(base.Items, null);
			}
			return sub;
		}
		/// <summary>
		/// Allows <see cref="Subscription"/> to remove itself when it is disposed.
		/// </summary>
		/// <param name="id"></param>
		void Unsubscribe(int id)
		{
			if (_disposed) return;

			lock (base.SyncRoot)
			{
				_subs.Remove(id);
			}
		}

		#endregion

		#region IDisposable

		bool _disposed;
		/// <summary>
		/// Disposes of the feed by cleaning up any remaining subscribers.
		/// </summary>
		public void Dispose()
		{
			if (_disposed) return;
			_disposed = true;
			lock(base.SyncRoot)
			{
				foreach (var sub in _subs.Values.ToArray()) sub.Dispose();
			}
		}

		#endregion
	}
}
